Загрузка библиотек:
import torch
import torchvision
import torchvision.transforms as transforms
import torch.nn as nn
# import torch.optim as optim
#import os
# import torch.nn.functional as F
import matplotlib.pyplot as plt
# from collections import defaultdict # to check distribution by classes
# from sklearn.metrics import precision_recall_fscore_support # to calculate F1 score
# from sklearn.model_selection import StratifiedShuffleSplit # to split images to train, val, and test
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
# device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
C:\Users\elena\AppData\Local\Programs\Python\Python310\lib\site-packages\tqdm\auto.py:22: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm
Загружаем лучший стейт обученной модели
transform = transforms.Compose([
transforms.Resize(64),
transforms.CenterCrop(64),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
image_dataset = torchvision.datasets.ImageFolder('../images', transform=transform)
image_dataloader = torch.utils.data.DataLoader(image_dataset, batch_size=32, shuffle=False)
model = torchvision.models.resnet18(weights='ResNet18_Weights.IMAGENET1K_V1')
model.fc = nn.Linear(512, len(image_dataset.classes))
model.load_state_dict(torch.load('../data/best_model_42_epoch_01_last.pt'))
image_classes = image_dataset.classes
Заменяем последний полносвязный уровень на единицы, чтобы извлечь признаки предпоследнего уровня для каждого изображения
model.fc_backup = model.fc
# model.fc = nn.Sequential()
model.fc = nn.Identity()
model.eval()
all_features = []
with torch.no_grad():
for inputs, labels in image_dataloader:
outputs = model(inputs)
features = outputs
all_features.append(features)
all_features = torch.cat(all_features, dim=0)
print(all_features)
print(all_features.size())
labels = np.concatenate([batch[1].numpy() for batch in image_dataloader])
tensor([[1.3572, 1.6053, 0.1278, ..., 2.3075, 0.6772, 3.0499],
[6.0935, 2.7589, 0.0000, ..., 3.3483, 4.4636, 0.0000],
[0.7585, 0.0000, 0.5369, ..., 0.4460, 0.0000, 0.0000],
...,
[0.0000, 0.0000, 1.3359, ..., 0.0670, 2.0176, 0.0000],
[0.0000, 1.7225, 2.1514, ..., 0.0000, 0.0000, 1.4539],
[0.0422, 2.4869, 1.3453, ..., 0.8349, 0.2712, 0.0000]])
torch.Size([1422, 512])
Полученная матрица (тензор) feature vector имеет размерность количество изображений (1422) * количество нейронов (512) на последнем уровне.
Сначала попробуем понизить размерность: используем PCA.
# используем StandardScaler для шкалирования наших данных
sc = StandardScaler()
X_scaled = sc.fit_transform(all_features)
# Apply PCA
pca = PCA(n_components=None)
pca.fit(X_scaled)
# Get the eigenvalues
# print("Eigenvalues:")
# print(pca.explained_variance_)
# print()
# Get explained variances
# print("Variances (Percentage):")
# print(pca.explained_variance_ratio_ * 100)
# print()
# Make the scree plot
plt.plot(np.cumsum(pca.explained_variance_ratio_ * 100))
plt.xlabel("Number of components (Dimensions)")
plt.ylabel("Explained variance (%)")
plt.hlines(y = 60, xmin = 0, xmax = 512, colors = 'g', linestyles = '--')
plt.show()
Можно увидеть, что первые главные компоненты объясняют достаточно мало дисперсии. Это значит, что признаки получились мало коррелированы между собой. Попробуем сделать кластеризацию методом к-средних на исходной матрице, количество кластеров выбираем 8, как и число классов изображения:
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=8, random_state=42, n_init=10)
kmeans.fit(all_features)
labels_kmeans = kmeans.labels_
centers = kmeans.cluster_centers_
from sklearn.metrics import adjusted_rand_score
labels == labels_kmeans
ari = adjusted_rand_score(labels, labels_kmeans)
print("Adjusted Rand Index: {:.4f}".format(ari))
Adjusted Rand Index: 0.1536
Для оценки точности используем функцию adjusted_rand_score, которая сама определяет наиболее вероятный лейбл кластера. Точность кластеризации получилась не очень высокая. Для визуализации и отрисовки лейблов используем UMAP.
import umap.umap_ as umap
embedding = umap.UMAP(n_neighbors=15,
min_dist=0.3,
metric='correlation').fit_transform(all_features)
# Plot the UMAP visualization with true labels
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(12, 6))
axes[0].scatter(embedding[:, 0], embedding[:, 1], c=labels, cmap='tab10', s=6)
axes[0].set_title('UMAP visualization with true labels')
# axes[0].colorbar()
# Plot the UMAP visualization with predicted labels
axes[1].scatter(embedding[:, 0], embedding[:, 1], c=labels_kmeans, cmap='tab10', s=6)
axes[1].set_title('UMAP visualization with predicted by kmeans labels')
# axes[1].colorbar()
# plt.colorbar(ax=axes)
fig.suptitle('k-means based on the original matrix, ARI = {:.4f}'.format(ari))
plt.show()
# plt.scatter(embedding[:, 0], embedding[:, 1], c=labels, cmap='tab10', s = 6)
# plt.colorbar()
# plt.title('UMAP visualization with true labels')
# plt.show()
# # Plot the UMAP visualization with predicted labels
# plt.scatter(embedding[:, 0], embedding[:, 1], c=labels_kmeans, cmap='tab10', s = 6)
# plt.colorbar()
# plt.title('UMAP visualization with predicted labels')
# plt.show()
Попробуем использовать алгоритм к-средних на матрице из первых 100 главных компонент
# from sklearn.decomposition import PCA
pca = PCA(n_components=100)
pca_data = pca.fit_transform(all_features)
# Perform k-means clustering on the PCA components
kmeans_pca = KMeans(n_clusters=8, random_state=42, n_init=10)
kmeans_pca.fit(pca_data)
# Print the cluster labels assigned to each data point
print(kmeans_pca.labels_)
ari_pca = adjusted_rand_score(labels, kmeans_pca.labels_)
print("Adjusted Rand Index: {:.4f}".format(ari_pca))
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(12, 6))
axes[0].scatter(embedding[:, 0], embedding[:, 1], c=labels, cmap='tab10', s=6)
axes[0].set_title('UMAP visualization with true labels')
# axes[0].colorbar()
# Plot the UMAP visualization with predicted labels
axes[1].scatter(embedding[:, 0], embedding[:, 1], c=kmeans_pca.labels_, cmap='tab10', s=6)
axes[1].set_title('UMAP visualization with predicted by kmeans labels')
fig.suptitle('k-means based on first 100 PC, ARI = {:.4f}'.format(ari_pca))
# axes[1].colorbar()
# plt.colorbar(ax=axes)
plt.show()
[3 4 3 ... 4 2 3] Adjusted Rand Index: 0.1516
Точность кластеризации не улучшилась после использования главных компонент, возможно потому что исходные переменные не были сильно скоррелированы друг с другом. Попробуем использовать иерархическую кластеризацию
# import numpy as np
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster
# Perform hierarchical clustering
Z = linkage(all_features, method='ward')
# Cut the dendrogram to 8 groups
labels_hier = fcluster(Z, 8, criterion='maxclust')
color_threshold = Z[-7, 2]
# Plot the dendrogram to visualize the hierarchy
dendrogram(Z, color_threshold=color_threshold)
plt.title('Dendrogram')
plt.xlabel('Sample index')
plt.ylabel('Distance')
plt.show()
# Compare the resulting labels with the true labels
score = adjusted_rand_score(labels, labels_hier)
print('Adjusted Rand score:', score)
Adjusted Rand score: 0.1625178276870089
Результат сопоставим с тем, что получилось алгоритмом k-means.
Теперь попробуем использовать метод k-ближайших соседей. Загрузим те же самые индексы тренировочного датасета и тестового + валидационного, чтобы избежать утечки данных.
train_index = np.load('../data/train_index_42.npy')
# train_index
test_index = np.load('../data/test_index_42.npy')
# print(test_index)
X_train = all_features[train_index]
X_test = all_features[test_index]
y_train = labels[train_index]
y_test = labels[test_index]
from sklearn.neighbors import KNeighborsClassifier
# from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
# k = 16 # сработало хорошо
k = 15
knn = KNeighborsClassifier(n_neighbors=k)
knn.fit(X_train, y_train)
y_pred = knn.predict(X_test)
report = classification_report(y_test, y_pred, zero_division=0)
print(report)
acc = np.mean(y_pred == y_test)
print("Classification accuracy: {:.2f}%".format(acc * 100))
precision recall f1-score support
0 1.00 0.16 0.27 19
1 0.55 0.92 0.69 110
2 0.57 0.71 0.63 72
3 0.76 0.21 0.33 61
4 0.57 0.66 0.61 65
5 0.69 0.69 0.69 35
6 1.00 0.22 0.36 23
7 0.72 0.31 0.43 42
accuracy 0.59 427
macro avg 0.73 0.48 0.50 427
weighted avg 0.66 0.59 0.55 427
Classification accuracy: 59.25%
Метод k-ближайших соседей отработал лучше, чем k-means, поэтому попробуем визуализировать картинки с соответствующим лейблом. По очереди отрисуем картинки, соответствующие предсказанным лейблам. Картинок в классах разное количество, поэтому по возможности будут отрисованы все, но максимум 36 изображений хорошо размещаются на одном полотне.
def show_images(indices, nrow=4,ncol=4, title = ''):
images = [image_dataset[idx][0] for idx in indices]
curr_labels = labels[indices]
curr_labels = (np.take(image_classes, curr_labels))
fig, axs = plt.subplots(nrows=nrow, ncols=ncol, figsize=(21, 12))
# plt.title('title')
# iterate over the subplots and plot the images and labels
for i, ax in enumerate(axs.flat):
# plot the image
imgs = images[i].numpy().transpose((1, 2, 0))
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
imgs = std * imgs + mean
imgs = np.clip(imgs, 0, 1)
ax.imshow(imgs)
ax.set_title(str(curr_labels[i]))
ax.axis("off")
fig.suptitle(title, fontsize=18)
plt.show()
print(test_index[y_pred == 0])
show_images(test_index[y_pred == 0], 1, len(test_index[y_pred == 0]), 'Для артдеко характерно наличие картинки по центру, по краям фон')
[52 28 23]
Первый класс изображений - предсказанный алгоритмом к-ближайших соседей стиль АртДеко. Определилось три изображения, все относятся к нужному классу. Можно отметить наличие изображения по центру, в то время как по краям фон. Возможно, это являлось признаком, на которые обращает внимание модель. Отрисуем следующий класс изображений: кубизм
show_images(test_index[y_pred == 1], 6, 6, 'Для кубизма сложно найти ярко выраженные фичи')
Видно, что многие изображения определились правильно, однако очень много ложно-позитивных срабатываний. В отличие от предыдущего случая, здесь изображения заполнены содержанием по всей площади. Цвета яркие. Я смотрела сама изображения в папке кубизм, туда попал в том числе Сезанн, который скорее в стиле постимпрессионизма пишет. Например, здесь это изображение во второй строке четвертого столбца (paul-cezanne_chateau-noir-1). Подобные изображения могли затруднить идентификацию признаков изображений классического кубизма (Малевич). Перейдем к импрессионизму
print(len(test_index[y_pred == 2]))
show_images(test_index[y_pred == 2], 6, 6, 'Для импрессионизма характерны изображения природы')
90
Особенности картин, которые классифицированы как импрессионизм ярко выделяются: изображения природы, деревьев, озер. По этой причине натурализм и фотографии, где изображена природа ошибочно классифицированы как импрессионизм. Например, изображение рококо в третьей строке 4 столбца, где изображены березы, на неискушенный глаз похоже на картину импрессиониста. Так что неудивительно, что нейросеть тоже ошиблась :)
print(len(test_index[y_pred == 3]))
show_images(test_index[y_pred == 3], 4, 4, 'Для японизма возможно ключевым признаком является бежевый фон')
17
Кажется что нейросеть определелиа для японизма характерным свойством бежевый светлый фон, возможно как в той истории про то как волков от собак отличали по снежному фону у волков. Это объясняет почему сюда попал кубизм (2 строка 3 столбец), птичка (3, 3) и розовая бутылка (3, 2)
show_images(test_index[y_pred == 4], 6, 6, 'В натурализме много растений, в особенности цветов')
Для натурализма характерны такие формы как цветы и листья. Есть несколько странных ложнопозитивных срабатываний, например машина или кот на ковре, что сложно интерпретировать. Однако в остальном можно увидеть общие черты изображений. Аисты (2 строка 2 столбец) похожи скорее на изображение природы, чем то что нейросеть воспринимает как рококо, подробнее ниже ->
show_images(test_index[y_pred == 5], 6, 5, 'Большинство изображений стиля рококо содержат лица')
Одно из самых ярких кластеров признаков - Рококо. Почти на всех изображениях есть человек, а точнее лицо. Это объясняет почему фотографии с лицами людей определились в рококо. Кроме того, любопытно что нейросеть распознает и группы людей, где лица некрупным планом. Можно сказать, что для нейросети определящим признаком, что изображение относится к рококо, является наличие людей.
show_images(test_index[y_pred == 6], 1, len(test_index[y_pred == 6]), 'Для комиксов (cartoon) характерны яркие цвета')
Картинок с мультиками немного, но они корректно определились. Характерным свойством является яркие, даже слишком цвета. К сожалению, выборка в исходном датасете небольшая, чтобы делать более конкретные выводы.
print(len(test_index[y_pred == 7],))
show_images(test_index[y_pred == 7], 3, 6, 'Для фотографий характерны изображения транспорта и коты')
18
Похоже для нейросети важным признаком фотографии являются машинки, мотициклы, в целом транспорт и коты. Некоторые изображения определились достаточно неочевидно, например артдеко и японизм, которых ложно записали сюда. Но в целом логика прослеживается.
Можно заметить, что извлеченные изображения достаточно контрастные, хотя кубизм оказался достаточно шумным, без ярко выраженных фичей. Возможно, стоит увеличить выборку и если есть возможность вручную переопределить некоторые спорные лейблы.
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import accuracy_score
from sklearn.metrics import confusion_matrix, f1_score
# Create a gradient boosting classifier
gb = GradientBoostingClassifier(n_estimators=100, learning_rate=0.1, max_depth=3, random_state=42)
# Train the classifier on the training set
gb.fit(X_train, y_train)
# Make predictions on the test set
y_pred = gb.predict(X_test)
confusion_matrix_gb = confusion_matrix(y_test, y_pred)
print('Confusion Matrix:')
print(confusion_matrix_gb)
# Calculate F1 score
f1 = f1_score(y_test, y_pred, average='weighted')
# Calculate the accuracy of the model
accuracy = accuracy_score(y_test, y_pred)
print('Accuracy: {:.4f}, F1-score wieghted: {:.4f}'.format(accuracy, f1))
Confusion Matrix: [[ 3 8 0 6 2 0 0 0] [ 1 93 4 3 2 0 2 5] [ 0 8 50 2 9 3 0 0] [ 0 12 3 38 4 0 1 3] [ 2 6 11 4 35 2 1 4] [ 0 0 6 0 8 20 0 1] [ 0 4 7 4 1 1 5 1] [ 0 5 6 1 4 4 1 21]] Accuracy: 0.6206, F1-score wieghted: 0.6051
dataframe = pd.DataFrame(confusion_matrix_gb, index=image_classes, columns=image_classes)
plt.figure(figsize=(8, 6))
# Create heatmap
sns.heatmap(dataframe, annot=True, cbar=None,cmap="YlGnBu",fmt="d")
plt.title("Confusion Matrix of gradient boosting"), plt.tight_layout()
plt.ylabel("True Class"),
plt.xlabel("Predicted Class")
plt.show()
Точность и F1-score алгоритма градиентного бустинга из библиотеки sklearn.ensemble чуть ниже чем у нейронной сети. Для нейросети в этом наборе параметров получилось: Точность: 0.6842, Weighted F1-Score: 0.6811
Попробую использовать алгоритм xgboost (Extreme Gradient Boosting) из одноименной библиотеки.
import xgboost as xgb
xgb_model = xgb.XGBClassifier(n_estimators=100, learning_rate=0.3, max_depth=3, random_state=42)
# Train the model
xgb_model.fit(X_train, y_train)
# Make predictions on the test set
y_pred = xgb_model.predict(X_test)
# Calculate the confusion matrix
confusion_matrix_xgb = confusion_matrix(y_test, y_pred)
print('Confusion Matrix:')
print(confusion_matrix_xgb)
# Calculate F1 score
f1 = f1_score(y_test, y_pred, average='weighted')
# print(f'F1 score: {f1:.3f}')
# Calculate the accuracy of the model
accuracy = accuracy_score(y_test, y_pred)
# print(f'Accuracy: {accuracy:.3f}')
print('Accuracy: {:.4f}, F1-score wieghted: {:.4f}'.format(accuracy, f1))
Confusion Matrix: [[ 5 9 0 1 2 0 2 0] [ 4 91 3 1 6 0 1 4] [ 0 13 43 2 10 3 1 0] [ 1 10 4 38 4 1 0 3] [ 1 5 9 5 39 2 0 4] [ 0 0 5 0 7 23 0 0] [ 0 4 4 5 0 0 8 2] [ 0 5 4 0 5 2 0 26]] Accuracy: 0.6393, F1-score wieghted: 0.6324
dataframe = pd.DataFrame(confusion_matrix_xgb, index=image_classes, columns=image_classes)
plt.figure(figsize=(8, 6))
# Create heatmap
sns.heatmap(dataframe, annot=True, cbar=None,cmap="YlGnBu",fmt="d")
plt.title("Confusion Matrix of xgboost"), plt.tight_layout()
plt.ylabel("True Class"),
plt.xlabel("Predicted Class")
plt.show()
Результаты xgboost лучше чем у градиентного бустинга из sklearn.ensemble. Однако softmax в нейросети имеет не меньшую точность и F1 score, по крайней мере при этом наборе параметров.